React Context 原理解析

React

前言

React 中,Context是一种高效的状态共享机制,能够避免组件层级较深时多层级props传递带来的问题。 通过Context,我们可以在组件树中共享数据,如主题、语言、认证信息等,而无需逐层手动传递。这种机制不仅提升了代码的可读性,还大幅简化了状态管理的复杂度。 ReactContext API16.3 版本引入后,经历了多次改进,其中包括性能优化和更灵活的使用方式。在最新版本中(React 18+),通过引入enableLazyContextPropagation等机制,Context的性能和更新逻辑得到了显著提升。本文将从基础概念、源码分析和性能优化等方面,深入探讨React中的Context

Context 基本使用

Context 的核心是通过 CreateConext 来创建一个 Context,然后提供 Provider, 供函数式组件 useContext 或者更古老的方式使用 Consumer 来读取共享数据,以下是一个简单的示例。

type TypeTheme = 'light' | 'dark'

const ThemeContext = createContext<{ theme: TypeTheme }>({
  theme: 'light',
})

const InnerComponent = () => {
  const { theme } = useContext(ThemeContext)

  return <>theme is {theme}</>
}

class OtherComponent extends Component<{ theme: TypeTheme }> {
  render() {
    return <>{this.props.theme}</>
  }
}

const App = () => {
  return (
    <ThemeContext.Provider value={{ theme }}>
      <InnerComponent></InnerComponent>
      <ThemeContext.Consumer>
        {({ theme }) => {
          return <OtherComponent theme={theme}></OtherComponent>
        }}
      </ThemeContext.Consumer>
    </ThemeContext.Provider>
  )
}

Context 底层实现

CreateConext 源码时创建一个 REACT_CONTEXT_TYPE 的对象,它的数据结构是:

function createContext<T>(defaultValue: T): ReactContext<T> {
  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    _threadCount: 0,
    Provider: (null: any),
    Consumer: (null: any),
  };

  if (enableRenderableContext) {
    context.Provider = context;
    context.Consumer = {
      $$typeof: REACT_CONSUMER_TYPE,
      _context: context,
    };
  } else {
    // 省略
  }
  return context;
}

在源码流程中, 当 Context 更新时,通过 pushProvider 来更新 Context 对象上的值。

export function pushProvider<T>(
  providerFiber: Fiber,
  context: ReactContext<T>,
  nextValue: T
): void {
  if (isPrimaryRenderer) {
    push(valueCursor, context._currentValue, providerFiber)
    context._currentValue = nextValue
  } else {
  }
}

Context 更新流程

虽然 useContextConsumer 在使用方式上有所区别,但是都是通过 readContext 来获取最新的值,而它们都是通过调用 readContextForConsumer。 当 Context 是嵌套结构时,则是通过类似于链表结构串联起来 Context

function readContextForConsumer<C>(
  consumer: Fiber | null,
  context: ReactContext<C>,
): C {
  const value = isPrimaryRenderer
    ? context._currentValue
    : context._currentValue2;

  const contextItem = {
    context: ((context: any): ReactContext<mixed>),
    memoizedValue: value,
    next: null,
  };

  if (lastContextDependency === null) {
    if (consumer === null) {
      throw new Error(
        'Context can only be read while React is rendering. ' +
          'In classes, you can read it in the render method or getDerivedStateFromProps. ' +
          'In function components, you can read it directly in the function body, but not ' +
          'inside Hooks like useReducer() or useMemo().',
      );
    }

    // This is the first dependency for this component. Create a new list.
    lastContextDependency = contextItem;
    consumer.dependencies = __DEV__
      ? {
          lanes: NoLanes,
          firstContext: contextItem,
          _debugThenableState: null,
        }
      : {
          lanes: NoLanes,
          firstContext: contextItem,
        };
    if (enableLazyContextPropagation) {
      consumer.flags |= NeedsPropagation;
    }
  } else {
    lastContextDependency = lastContextDependency.next = contextItem;
  }
  return value;
}

Context 变化时,在 beginWork 流程中通过 checkScheduledUpdateOrContext 来完成判断走更新逻辑。

propsConetxt 以及更新优先级的变化,会导致组件的 re-render

function checkScheduledUpdateOrContext(current: Fiber, renderLanes: Lanes): boolean {
  // 判断更新优先级
  const updateLanes = current.lanes
  if (includesSomeLane(updateLanes, renderLanes)) {
    return true
  }
  // 判断 Conext是否发生变化
  if (enableLazyContextPropagation) {
    const dependencies = current.dependencies
    if (dependencies !== null && checkIfContextChanged(dependencies)) {
      return true
    }
  }
  return false
}

checkIfContextChanged 用来比较上下文的值是否更新。

export function checkIfContextChanged(currentDependencies: Dependencies): boolean {
  if (!enableLazyContextPropagation) {
    return false
  }
  let dependency = currentDependencies.firstContext
  while (dependency !== null) {
    const context = dependency.context
    const newValue = isPrimaryRenderer ? context._currentValue : context._currentValue2
    const oldValue = dependency.memoizedValue
    if (
      enableContextProfiling &&
      dependency.select != null &&
      dependency.lastSelectedValue != null
    ) {
      if (
        checkIfSelectedContextValuesChanged(
          dependency.lastSelectedValue,
          dependency.select(newValue)
        )
      ) {
        return true
      }
    } else {
      if (!is(newValue, oldValue)) {
        return true
      }
    }
    dependency = dependency.next
  }
  return false
}

Context 的变化

相对于之前 Context 变化时,需要 DFS 遍历 Fiber 树,找到依赖的 Fiber 节点,然后创建更新/更新优先级。 最新的 Context 采用 enableLazyContextPropagation1,减少 Context 不必要的更新传播, 降低了遍历所有 dependencies 带来的性能开销。 以下是早期遍历流程的代码片段:

function propagateContextChange_eager<T>(
  workInProgress: Fiber,
  context: ReactContext<T>,
  renderLanes: Lanes,
): void {
  if (enableLazyContextPropagation) {
    return;
  }
  let fiber = workInProgress.child;
  if (fiber !== null) {
    fiber.return = workInProgress;
  }
  while (fiber !== null) {
    let nextFiber;

    // DFS 开启遍历找到依赖当前 Context 的节点,并创建更新/更新优先级
    const list = fiber.dependencies;
    if (list !== null) {
      nextFiber = fiber.child;

      let dependency = list.firstContext;
      while (dependency !== null) {
        if (dependency.context === context) {
          if (fiber.tag === ClassComponent) {
            const lane = pickArbitraryLane(renderLanes);
            const update = createUpdate(lane);
            update.tag = ForceUpdate;
            const updateQueue = fiber.updateQueue;
            if (updateQueue === null) {
            } else {
              const sharedQueue: SharedQueue<any> = (updateQueue: any).shared;
              const pending = sharedQueue.pending;
              if (pending === null) {
                update.next = update;
              } else {
                update.next = pending.next;
                pending.next = update;
              }
              sharedQueue.pending = update;
            }
          }

          // 更新优先级
          fiber.lanes = mergeLanes(fiber.lanes, renderLanes);
          const alternate = fiber.alternate;
          if (alternate !== null) {
            alternate.lanes = mergeLanes(alternate.lanes, renderLanes);
          }
          scheduleContextWorkOnParentPath(
            fiber.return,
            renderLanes,
            workInProgress,
          );

          list.lanes = mergeLanes(list.lanes, renderLanes);

          break;
        }
        dependency = dependency.next;
      }
    } else if (fiber.tag === ContextProvider) {
      nextFiber = fiber.type === workInProgress.type ? null : fiber.child;
    } else if (fiber.tag === DehydratedFragment) {
      // 省略
    } else {
      nextFiber = fiber.child;
    }
    if (nextFiber !== null) {
      nextFiber.return = fiber;
    } else {
      nextFiber = fiber;
      while (nextFiber !== null) {
        if (nextFiber === workInProgress) {
          nextFiber = null;
          break;
        }
        const sibling = nextFiber.sibling;
        if (sibling !== null) {
          sibling.return = nextFiber.return;
          nextFiber = sibling;
          break;
        }
        nextFiber = nextFiber.return;
      }
    }
    fiber = nextFiber;
  }
}

而现在在组件更新时,根据 checkScheduledUpdateOrContext 判断是否更新可以更加颗粒化的走更新逻辑。

总结

ContextReact 提供的高效共享状态的解决方案, 尽管最新版本通过 enableLazyContextPropagation 显著优化了更新的性能,减少了不必要的重渲染,但 Context 的值变化仍可能触发相关组件的重新渲染。 因此,在实际开发中,需谨慎管理 Context 的更新范围,避免在高频变化的场景中滥用全局状态。

参考

  1. React enableLazyContextPropagation